DearMiku

Opengl ES 3.0 on iOS--- HelloWord(绘制彩色矩形)

字数统计: 4.4k阅读时长: 17 min
2017/11/27 Share

简介

本文记录了我初学Opengl 绘制彩色矩形的过程,可能我对内容的描述不够准确,还请多多指正

实现

配置图层

1
2
3
+(Class)layerClass{
return [CAEAGLLayer class];
}

将当前View的Layer替换成 CAEAGLLayer类,opengl的绘制内容也是在该View上显示的.

1
2
3
4
//设置不透明度为YES,因为透明图层性能不好
self.layer.opaque = YES;
self.layer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];

可以对CAEAGLLayer进行额外属性的配置:

kEAGLDrawablePropertyRetainedBacking 传入布尔值,表示是否保持绘制状态,若设置为NO,则下次将重新绘制.
kEAGLDrawablePropertyColorFormat 设置layer的颜色缓冲区格式,EAGLContext对象 使用此格式来创建渲染缓冲区的存储.
kEAGLColorFormatRGB565 —> 16bit RGB格式
kEAGLColorFormatRGBA8 —> 32-bit RGBA格式

配置上下文

1
2
3
4
5
6
7
8
9
    _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
if (!_context) {
return;
}
// 将当前上下文设置为我们创建的上下文
if (![EAGLContext setCurrentContext:_context]) {
return;
}
}

设置缓冲区(渲染缓冲和帧缓冲)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//在缓冲区中返回n个渲染缓冲对象句柄,不保证这些句柄是连续的整数,但是肯定没有被使用.
GLuint renderbuffer[1];
glGenRenderbuffers(ARRAY_SIZE(renderbuffer), renderbuffer);

//将缓冲区对象和句柄 绑定到指定的缓冲区目标.
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer[0]);

//检验是否创建绑定成功
if (glIsRenderbuffer(renderbuffer[0]) == GL_TRUE) {
NSLog(@"成功生成渲染缓存");
}
//为缓冲区对象分配存储空间.
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];

//设置帧缓冲区(Frame Buffer),和渲染缓冲区大致相同
GLuint framebuffer[1];
glGenFramebuffers(ARRAY_SIZE(framebuffer), framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer[0]);
if (glIsFramebuffer(framebuffer[0]) == GL_TRUE) {
NSLog(@"成功绑定帧缓存");
}

//将相关的buffer依附到 帧缓存上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer[0]);

//释放渲染缓存
//glDeleteRenderbuffers(ARRAY_SIZE(renderbuffer), renderbuffer);
//释放帧缓存
//glDeleteFramebuffers(ARRAY_SIZE(framebuffer), framebuffer);

渲染缓存: 是OpenGL ES管理的一块高效内存区域,渲染缓存的数据只有关联一个帧缓存对象才有意义,并且需要保证图像缓存格式 必须与OpenGL ES要求的渲染格式相符.

帧缓存:它是屏幕所显示画面的一个直接映象,又称为位映射图(Bit Map)或光栅。帧缓存的每一存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。

函数解释

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer[0]);
参数:

target: 指定的帧缓冲区目标 必须是 GL_DRAW_FRAMEBUFFER, GL_READ_FRAMEBUFFER, 或 GL_FRAMEBUFFER. (GL_FRAMEBUFFER = GL_DRAW_FRAMEBUFFER);
attachment: 帧缓存对象依附的目标 GL_COLOR_ATTACHMENT(0~i) —> 第i个颜色缓存 0为默认值, GL_DEPTH_ATTACHMENT —> 深度缓存, GL_STENCIL_ATTACHMENT —> 模板缓存
renderbuffertarget :必须为 GL_RENDERBUFFER,指定的渲染缓存区目标
renderbuffer: 渲染缓冲区对象句柄.

准备着色器源码

OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线.OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线

着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

顶点着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#version 300 es //OpenGL ES 3.0

//接受的输入变量
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;

//输出变量
out vec3 outColor;

//相当于C语言的main函数
void main()
{
//绘制图形
gl_Position = vec4(position[0],position[1],position[2], 1.0);

outColor = color;
}

图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入.一个顶点(Vertex)是一个3D坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据.

片段着色器

1
2
3
4
5
6
7
8
9
10
11
12
#version 300 es

precision mediump float; //表示 数据精确度 这里设置的为中级

in vec3 outColor;

out vec4 FragColor; //输出的色彩

void main()
{
FragColor = vec4(outColor.x,outColor.y,outColor.z, 1.0);;
}

片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色

在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

创建着色器对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static GLuint createGLShader(const char *shaderText, GLenum shaderType)
{
//创建着色器,将根据传入的type参数 创建一个新的 顶点或片段着色器,返回值为新的着色器对象句柄
//GL_VERTEX_SHADER(顶点着色器) GL_FRAGMENT_SHADER(片段着色器)
GLuint shader = glCreateShader(shaderType);

//为着色器对象 提供着色器源代码.
//参数: shader --> 着色器对象句柄
// count --> 着色器源字符串数量
// string --> 字符串的数组指针
// length ---> 指向保存美工着色器字符串大小且元素数量为count的整数数组指针.如果length为NULL 着色器字符串将被认定为空.
glShaderSource(shader, 1, &shaderText, NULL);

//调用该方法,将指定的着色器源代码 进行编译
//参数shader 为着色器句柄
glCompileShader(shader);

//调用该方法获取 着色器源代码编译是否成功,并获取其他相关信息
//第二个参数 pname 表示要查询什么信息
/*
GL_COMPILE_STATUS ---> 是否编译成功 成功返回 GL_TRUE
GL_INFO_LOG_LENGTH ---> 查询源码编译后长度
GL_SHADER_SOURCE_LENGTH ---> 查询源码长度
GL_SHADER_TYPE ---> 查询着色器类型()
GL_DELETE_STATUS ---> 着色器是否被标记删除
*/
int compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char *infoLog = (char *)malloc(sizeof(char) * infoLen);
if (infoLog) {


//检索信息日志
//参数: shader 着色器对象句柄
// maxLength 保存信息日志的缓冲区大小
// length 写入信息日志长度 ,不需要知道可传NULL
// infoLog 保存日志信息的指针
glGetShaderInfoLog (shader, infoLen, NULL, infoLog);
GLlog("Error compiling shader: %s\n", infoLog);
free(infoLog);
}
}
//删除着色器对象, 参数shader为要删除的着色器对象的句柄
//若一个着色器链接到一个程序对象,那么该方法不会立刻删除着色器,而是将着色器标记为删除,当着色器不在连接到任何程序对象时,它的内存将被释放.
glDeleteShader(shader);
return 0;
}

return shader;
}

创建程序对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 创建一个程序对象,返回程序对象的句柄
GLuint program = glCreateProgram();

// 得到需要的着色器
GLuint vertShader = createGLShader(vertext, GL_VERTEX_SHADER); //顶点着色器
GLuint fragShader = createGLShader(frag, GL_FRAGMENT_SHADER); //片元着色器

if (vertShader == 0 || fragShader == 0) {
return 0;
}

//将程序对象和 着色器对象链接 //在ES 3.0中,每个程序对象 必须连接一个顶点着色器和片段着色器
//program程序对象句柄 shader着色器句柄
glAttachShader(program, vertShader);
glAttachShader(program, fragShader);


//链接程序对象 生成可执行程序(在着色器已完成编译 且程序对象连接了着色器)
//链接程序会检查各种对象的数量,和各种条件.
//在链接阶段就是生成最终硬件指令的时候(和C语言一样)
glLinkProgram(program);


//检查链接是否成功
GLint success;
glGetProgramiv(program, GL_LINK_STATUS, &success);


if (!success) {
GLint infoLen;
//使用 GL_INFO_LOG_LENGTH 表示获取信息日志
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
GLchar *infoText = (GLchar *)malloc(sizeof(GLchar)*infoLen + 1);
if (infoText) {
memset(infoText, 0x00, sizeof(GLchar)*infoLen + 1);

// 从信息日志中获取信息
glGetProgramInfoLog(program, infoLen, NULL, infoText);
GLlog("%s", infoText);
free(infoText);

//此函数用于校验当前的程序对象,校验结果可通过 glGetProgramiv函数检查,此函数只用于调试,因为他很慢.
//glValidateProgram(program);
}
}
glDeleteShader(vertShader);
glDeleteShader(fragShader);

//删除程序对象
glDeleteProgram(program);
return 0;
}

/*
* 链接完着色器,生成可执行程序. 将着色器断开删除
*/
//断开指定程序对象和片段着色器
glDetachShader(program, vertShader);
glDetachShader(program, fragShader);

//将着色器标记为删除
glDeleteShader(vertShader);
glDeleteShader(fragShader);

程序对象就是一个容器对象,将着色器与之连接,最后链接生成最终的可执行程序.

输入顶点数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//三角形的三点坐标+颜色坐标
static GLfloat vertices[] = {
//点坐标 //颜色
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f
};

static unsigned int indices[] = {
0,1,3,
1,2,3
};

unsigned int VAO,VBO,EBO;
//创建VAO对象,VBO对象,EBO对象
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

//绑定VAO VBO EBO
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

将顶点数据 和 索引数据 复制到缓冲区中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

//设置顶点属性指针 输入数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
//激活 0号变量,为了性能,若不激活着色器无法接受数据
glEnableVertexAttribArray(0);

glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);

VAO VBO EBO

不使用VAO VBO绘制代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static GLfloat vertices[] = {
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
GLint posSlot = glGetAttribLocation(_program, "position");
glVertexAttribPointer(posSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(posSlot);

static GLfloat colors[] = {
0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f
};
GLint colorSlot = glGetAttribLocation(_program, "color");
glVertexAttribPointer(colorSlot, 3, GL_FLOAT, GL_FALSE, 0, colors);
glEnableVertexAttribArray(colorSlot);

VBO

如上面的例子所示, 普通的顶点数组的传输,需要在绘制的时候频繁地从CPU到GPU传输顶点数据,这种做法效率低下.
为了加快显示速度,显卡增加了一个扩展 VBO (Vertex Buffer object),即顶点缓存。它直接在 GPU 中开辟一个缓存区域来存储顶点数据,因为它是用来缓存储顶点数据,因此被称之为顶点缓存。使用顶点缓存能够大大较少了CPU到GPU 之间的数据拷贝开销,因此显著地提升了程序运行的效率。

函数

1, 创建顶点缓存对象
void glGenBuffers (GLsizei n, GLuint* buffers);

参数 n : 表示需要创建顶点缓存对象的个数
参数 buffers :用于存储创建好的顶点缓存对象句柄

2, 将顶点缓存对象设置为当前数组缓存对象
void glBindBuffer (GLenum target, GLuint buffer);

target :指定绑定的目标,取值为 GL_ARRAY_BUFFER(用于顶点数据) 或 GL_ELEMENT_ARRAY_BUFFER(用于索引数据)
buffer :顶点缓存对象句柄

3, 为顶点缓存对象分配空间(这里就是将数据一次性 拷贝至显存中)
void glBufferData (GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage);

target:指定绑定的目标,取值为 GL_ARRAY_BUFFER(用于顶点数据) 或 GL_ELEMENT_ARRAY_BUFFER(用于索引数据).
size :指定顶点缓存区的大小,以字节为单位计数;
data :用于初始化顶点缓存区的数据,可以为 NULL,表示只分配空间,之后再由 glBufferSubData 进行初始化;
usage :表示该缓存区域将会被如何使用,它的主要目的是用于对该缓存区域做何种程度的优化,比如经常修改的数据可能就会放在GPU缓存中达到快速操作的目的.

usage:

1
2
3
GL_STATIC_DRAW 	表示该缓存区不会被修改
GL_DYNAMIC_DRAW 表示该缓存区会被周期性更改
GL_STREAM_DRAW 表示该缓存区会被频繁更改

4,更新顶点缓冲区数据
void glBufferSubData (GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data);

offset: 表示需要更新的数据的起始偏移量;
size: 表示需要更新的数据的个数,也是以字节为计数单位;
data: 用于更新的数据;

5,释放顶点缓存
void glDeleteBuffers (GLsizei n, const GLuint* buffers);

n : 表示顶点缓存对象的个数
buffers :顶点缓存对象句柄

VAO

VAO的全名是 Vertex Array Object。它不用作存储数据,但它与顶点绘制相关。
它的定位是状态对象,记录存储状态信息。VAO记录的是一次绘制中做需要的信息,这包括数据在哪里、数据格式是什么等信息。VAO其实可以看成一个容器,可以包括多个VBO。 由于它进一步将VBO容于其中,所以绘制效率将在VBO的基础上更进一步。目前OpenGL ES3.0及以上才支持顶点数组对象。

VBO和VAO关系

函数

1, 创建顶点数组对象
glGenVertexArrays (GLsizei n, GLuint* arrays) ;

n : 表示顶点数组对象的个数
arrays :顶点数组对象句柄

2, 将顶点数组对象设置为当前顶点数组对象
glBindVertexArray (GLuint array) ;

arrays :顶点数组对象句柄

3,释放顶点数组对象
glDeleteVertexArrays (GLsizei n, const GLuint* arrays);

n : 表示顶点数组对象的个数
arrays :顶点数组对象句柄

使用

如代码中所写,在绑定VAO后,后续的VBO操作都会存储到当前绑定的VAO中.这样就将当前绘制状态记录下来了. 当下次还要绘制当前图形时, 只需再次绑定当前VAO, 进行后面的绘制操作即可.对于OpenGL ES2.0 使用VAO 则需要使用另外提供的API来实现.

1
2
3
4
GLvoid glGenVertexArraysOES(GLsizei n, GLuint *arrays)
GLvoid glBindVertexArrayOES(GLuint array);
GLvoid glDeleteVertexArraysOES(GLsizei n, const GLuint *arrays);;
GLboolean glIsVertexArrayOES(GLuint array);

EBO

索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO),当绘制现有图形时,存在顶点数据复用时,可以使用EBO. 但是这是需要斟酌的,因为在使用EBO时,在显存中 又存储的索引数据,有可能并不比原来性能更好~

函数

1,创建EBO(和VBO类似)

1
2
unsigned int EBO;
glGenBuffers(1, &EBO);

2,绑定EBO,将索引复制到缓冲里

1
2
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

3, 使用glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);替代glDrawArrays

函数补充

glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)

该函数用于将顶点属性传入顶点着色器
参数:

index: 对应顶你个点着色器中变量的location
size :表示该顶点属性对应的分量数量.也就是接收者为几位向量 如写入3 则表示为vec3 接收者为3维向量. 必须是 1~4.
type :表明每个分量的类型 可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT
normalized: 是否对每个分量进行归一化处理, 也就是若type为float类型.
stride:指定连续顶点属性之间的偏移量,如果设置0,则表示各个分量是紧密排在一起,中间没有其他多余数据.
ptr 顶点数据指针

此函数在有无VBO的情况下,使用有所差异~,在不适用VBO时,ptr确实是顶点数据指针.
当使用VBO时,顶点数据都已经拷贝至显存中,这里的ptr 就表示为缓冲区数据的便宜量了.

无EBO:
glVertexAttribPointer(colorSlot, 3, GL_FLOAT, GL_FALSE, 0, colors);

有EBO:

1
2
3
4
5
6
7
8
9
10
11
static GLfloat vertices[] = {
//点坐标 //颜色
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f
};
.......(VBO与其他代码).......

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));

在这里0号属性和1号属性紧密相连,且0号和1号的分量数都为3,以0号属性开头.
故: 第一个0号和第二个0号 中间有6个间距 stride = 6sizeof(float).
1号在0号后面, 0号ptr为 `(void
)0. 1号ptr为(void )(3sizeof(float))`

绘制

使用 EBO:

glDrawElements(GL_TRIANGLE_STRIP, 6, GL_UNSIGNED_INT, 0);
参数:
model:指定呈现那种图元(将这些点绘制成怎样的形状). 可选项:

GL_POINTS(点),
GL_LINE_STRIP(多端线),
GL_LINE_LOOP(线圈),
GL_LINES(线段),
GL_TRIANGLE_FAN, (三角形扇)
GL_TRIANGLES, (三角形)
count: 传入顶点数据的数量
type: 索引数组的元素属性 GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT.
indices: 指向索引数组的指针, 当使用VBO时,则表示为偏移量,若为紧密相连时则传入0.

不使用EBO

glDrawArrays(GL_TRIANGLES, 0, 3);
参数: model 和上面那个含义一样.
first 表示顶点数据起始索引, 从头开始则为0.
count 表示要传入顶点数据的数量.

最后显示

[_context presentRenderbuffer:GL_RENDERBUFFER];

学习参考

你好三角形
OpenGL 简书专题

CATALOG
  1. 1. 简介
  2. 2. 实现
    1. 2.1. 配置图层
    2. 2.2. 配置上下文
    3. 2.3. 设置缓冲区(渲染缓冲和帧缓冲)
      1. 2.3.1. 函数解释
    4. 2.4. 准备着色器源码
      1. 2.4.1. 顶点着色器
      2. 2.4.2. 片段着色器
    5. 2.5. 创建着色器对象
    6. 2.6. 创建程序对象
    7. 2.7. 输入顶点数据
      1. 2.7.1. VAO VBO EBO
        1. 2.7.1.1. VBO
          1. 2.7.1.1.1. 函数
        2. 2.7.1.2. VAO
          1. 2.7.1.2.1. 函数
          2. 2.7.1.2.2. 使用
        3. 2.7.1.3. EBO
          1. 2.7.1.3.1. 函数
      2. 2.7.2. 函数补充
    8. 2.8. 绘制
      1. 2.8.1. 使用 EBO:
      2. 2.8.2. 不使用EBO
      3. 2.8.3. 最后显示
  3. 3. 学习参考